package Roteamento;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;

/**
 * Classe que representa uma Tabela de Roteamento, seguindo  o algorítmo de Bellman-Ford.
 * @author David, Iury, Milena
 *
 */
public class TabelaRoteamento {

	
	HashMap<String,VetorRoteamento> matrix = new HashMap<String, VetorRoteamento>();
	List<Enlace> enlaces = new ArrayList<Enlace>();
	String roteadorId;
	private final int limiteDeInf = 1000; 
	private final int seguranca = 950;
	
	/**
	 * Cria uma nova tabela de Roteamento
	 * @param roteadorId id do roteador ao qual a tabela se refere
	 * @param enlaceList lista de enlaces ligados a este roteador
	 */
	public TabelaRoteamento(String roteadorId, Enlace[] enlaceList) {
		this.roteadorId = roteadorId;
		adicionaEnlacesNaLista(enlaceList);
		InicializaLinhas(roteadorId, enlaceList);
		ColocaValoresNasLinhas(roteadorId, enlaceList);
	}

	/**
	 * Adiciona novos enlaces na lista de enlaces do roteador
	 * @param enlaceList novos enlaces a serem adicionados
	 */
	private void adicionaEnlacesNaLista(Enlace[] enlaceList) {
		for(Enlace enlace : enlaceList){
			this.enlaces.add(enlace);
		}
	}

	/**
	 * Inicializa os valores das colunas a partir dos valores do enlace.
	 * Caso os enlaces nao deem informacoes para preencher todos os espacos da tabela,
	 * ent�o sera colocado o valor infinito nesta posicao
	 * 
	 * @param roteadorId - o id do roteador principal da tabela
	 * @param enlaceList - a lista de enlaces a qual o roteador principal esta conectado
	 */
	private void ColocaValoresNasLinhas(String roteadorId, Enlace[] enlaceList) {
		//inicializa todos os valores da matriz com o valor infinito		
		Collection<String> keys = matrix.keySet();
		for (String string : keys) {
			for (String string2 : keys) {
				matrix.get(string).updateDestino(string2, limiteDeInf);
			}
		}
		
		//o custo do roteador pra ele mesmo � zero
		matrix.get(roteadorId).updateDestino(roteadorId, 0);
		
		//atualiza o menor custo para os vizinho
		//conforme o custo do enlace
		for(Enlace enlace: enlaceList){
			if(enlace.getVizinhoId(roteadorId)!= null){
				matrix.get(roteadorId).updateDestino(enlace.getVizinhoId(roteadorId), enlace.getCusto());
			}
		}
	}

	/**
	 * Inicializa as linhas ta tabela. Inicialmente nao haverao valores para as colunas 
	 * @param roteadorId - o id do roteador principal da tabela
	 * @param enlaceList - a lista de enlaces a qual o roteador principal esta conectado
	 */
	private void InicializaLinhas(String roteadorId, Enlace[] enlaceList) {
		matrix.put(roteadorId, new VetorRoteamento(roteadorId));
		for (Enlace enlace : enlaceList) {
			String vizinho = enlace.getVizinhoId(roteadorId);
			if(vizinho != null){
				matrix.put(vizinho, new VetorRoteamento(vizinho));
			}
		}
	}

	/**
	 * Retorna os vizinhos do roteador.
	 * @return Lista com os ids dos vizinhos do roteador
	 */
	public List<String> getVizinhos() {
		List<String> returnList = new ArrayList<String>();
		for(String key : matrix.keySet()){
			returnList.add(key);
		}
		Collections.sort(returnList);
		return returnList;
	}

	/**
	 * Se ninguém consegue chegar a um nó com um custo menor que infinito, então o nó não é alcaçável.
	 * @return id dos nós alcancaveis
	 */
	public List<String> getDestinosAlcancaveis() {
		List<String> list = new ArrayList<String>();
		for(String vizinho: this.getVizinhos()){
			for(String destino: matrix.get(vizinho).getDestinosAlcancaveis()){
				if((!list.contains(destino))&&(matrix.get(vizinho).getMenorCusto(destino)<= seguranca)){
					list.add(destino);					
				} 
			}
		}
		Collections.sort(list);
		return list;
	}

	/**
	 * Retorna o menor custo entre dois dados roteadores
	 * @param idFonte id do roteador fonte
	 * @param idDestino id do roteador destino
	 * @return menor custo entre dois roteadores
	 */
	public int getMenorCusto(String idFonte, String idDestino) {
		return matrix.get(idFonte).getMenorCusto(idDestino);
	}

	/**
	 * Atualiza o vetor roteamento com os menores custos para seus vizinhos
	 * @param vetorRoteamento atual
	 * @return true caso houve mudança e false caso contrário
	 */
	public boolean update(VetorRoteamento vetorRoteamento) {
		vetorRoteamento = this.preencheVetor(vetorRoteamento);
		for (String vizinho : this.getVizinhos()) {
			if(vizinho.equals(vetorRoteamento.fonte)){
				matrix.put(vizinho, vetorRoteamento);
			} else{
				matrix.get(vizinho).atualizaDestinosAlcancaveis(vetorRoteamento);
			}
			
		}
		return calcularNovosCustosVetorPrincipal();
	}

	/**
	 * Preenche o vetorRoteamento com os novos valores
	 * @param vetorRoteamento atual
	 * @return vetorRoteamento atualizado
	 */
	private VetorRoteamento preencheVetor(VetorRoteamento vetorRoteamento) {
		for(String destino : this.getDestinosAlcancaveis()){
			if(! vetorRoteamento.getDestinosAlcancaveis().contains(destino)){
				vetorRoteamento.updateDestino(destino, limiteDeInf);
			}			
		}
		return vetorRoteamento;
	}

	/**
	 * Calcula os novos custos para o vetor roteamento a ser enviado para os outros nós
	 * @return true se ouve alguma mudança e false caso contrário
	 */
	private boolean calcularNovosCustosVetorPrincipal() {
		VetorRoteamento vetorPrincipal = matrix.get(roteadorId);
		boolean ocorreuMudanca = false;
		for(String destino: this.getDestinosAlcancaveis()){
			if (!destino.equals(roteadorId)) {
				int menorCusto = limiteDeInf;
				int valorAntigo = vetorPrincipal.getMenorCusto(destino);

				if (this.getVizinhos().contains(destino)) {
					menorCusto = this.getCustoEnlace(destino);
				}

				for (String vizinho : this.getVizinhos()) {
					if  (!vizinho.equals(roteadorId)) {
						if ((matrix.get(vizinho).getMenorCusto(destino) != limiteDeInf)
								&& (matrix.get(vizinho).getMenorCusto(destino)
										+ this.getCustoEnlace(vizinho) < menorCusto)) {

							menorCusto = matrix.get(vizinho).getMenorCusto(
									destino)
									+ this.getCustoEnlace(vizinho);
						}
					}
				}

				if(valorAntigo != menorCusto){
					ocorreuMudanca = true;
				}
				vetorPrincipal.updateDestino(destino, menorCusto);
			}
		}
		return ocorreuMudanca;
	}

	/**
	 * Retorna o custo para um roteador
	 * @param destino id do roteador de destino
	 * @return custo de um roteador para um outro vizinho
	 */
	private int getCustoEnlace(String destino) {
		for(Enlace enlace : enlaces){
			if(enlace.containsRoteador(destino) && (enlace.containsRoteador(roteadorId))){
				return enlace.getCusto();
			}
		}
		return limiteDeInf;
	}

	/**
	 * Pega os dados a serem enviados quando os custos para chegar em um outro nó, deste roteador, mudam
	 * @return VetorRoteamento com os novos custos 
	 */
	public VetorRoteamento getDadosASeremEnviados() {
		VetorRoteamento vetorDeRetorno = new VetorRoteamento(roteadorId);
		
		//retorna todos os destinos do vetor principal da tabela
		//com excecao daqueles que tem custo infinito
		for(String destino : matrix.get(roteadorId).getDestinosAlcancaveis()){
			if(matrix.get(roteadorId).getMenorCusto(destino)!= limiteDeInf){
				vetorDeRetorno.updateDestino(destino, matrix.get(roteadorId).getMenorCusto(destino));
			}
		}
		return vetorDeRetorno;
	}

	/**
	 * Adiciona um novo vizinho do roteador
	 * @param roteador novo vizinho
	 * @param custo custo para este novo vizinho
	 */
	public void adicionaNovoVizinho(String roteador, int custo) {
		if (!matrix.containsKey(roteador)) {
			this.enlaces.add(new Enlace(this.roteadorId, roteador, custo));
			
			// adiciona o novo vizinho com um vetor vazio
			matrix.put(roteador, new VetorRoteamento(roteador));

			// atualiza o caminho do roteador ate o novo vizinho
			matrix.get(roteadorId).updateDestino(roteador, custo);

			// atualiza a tabela
			this.update(this.getDadosASeremEnviados());
		}		
	}

	/**
	 * Remove um nó da tabela, junto com os seus custos
	 * @param roteadorASerRemovido id do roteador a ser removido
	 */
	public void removerVizinho(String roteadorASerRemovido) {
		//simula a queda do enlace do roteador principal da tabela com o roteador a ser removido
		for (int i = 0; i < enlaces.size(); i++) {
			if(enlaces.get(i).containsRoteador(roteadorASerRemovido)&&(enlaces.get(i).containsRoteador(roteadorId))){
				enlaces.remove(i);
				i--;
			}
		}
		
		//remove a linha do roteador
		matrix.remove(roteadorASerRemovido);
		
		calcularNovosCustosVetorPrincipal();
	}

	/**
	 * Gera a tabela de roteamento como uma matriz para poder ser apresentada na parte gráfica
	 * @return matriz n por n , onde n é o número de nós alcançáveis, com os custos para cada nó
	 */
	public String[][] getTabelaComoMatriz() {
		String[][] retorno = new String[this.getVizinhos().size()*this.getDestinosAlcancaveis().size()][3];
		int i=0;
		for(String vizinho: this.getVizinhos()){
			for(String destino: this.getDestinosAlcancaveis()){
				retorno[i][0] = vizinho;
				retorno[i][1] = destino;
				retorno[i][2] = Integer.toString(this.getMenorCusto(vizinho, destino));
				i++;
			}
		}
		return retorno;
	}

	/**
	 * @return a lista de enlaces ligados ao roteador.
	 */
	public List<Enlace> getEnlaces() {
		return enlaces;
	}

}
